Public Class UserAppsToSelect
    Friend Process As System.Diagnostics.Process

    ' TODO: Determine if I need "before" and "after" collections, and if so, whether to call them ProcessesToDisplayToUser and ProcessesToShowUser.
    ' TODO: If I need before and after collections, determine whether I should pass references to the user dialog ByVal, or if it's just as safe to pass them ByRef.  If ByVal, then I'll have to populate ProcessesToShowUser by pulling each Process object from ProcessesToDisplayToUser using the "ProcessName" (ByVal) that's passed back from the user dialog.
    ' TODO: If I need to pass processes ByVal to the user dialog, how will I be able to enumerate all the other Properties of the Processes?

    ' Create a shared Collection for all Process objects to be displayed in the CheckedListBox, so they can be re-used when populating the RunOnce Registry values.
    Friend ProcessesToShowUser As New Collection
    ' Note: I'd like to store three values for each row:
    '     one for the process with Process datatype
    '     one for the processname with String datatype
    '     one for the process Descriptive Name with String datatype
    ' RESEARCH: Is it possible to store multiple objects with different data types in a Collection?
    ' RESEARCH: Find a better array than Collection e.g. System.Collections.Specialized.NameValueCollection, so I can use all values as indices or data

    ' AppsNeverDisplayed: This static collection lists all those apps that will be excluded from the UserAppsToDisplay collection
    Friend ReadOnly AppsNeverDisplayed As String() = {"cmd", "explorer", "taskmgr", "communicator", "msnmsgr"}
    ' RESEARCH: is there a more efficient way to store this data than a Collection, since it never changes at RunTime?
    ' TODO: v3 - provide an external input (Registry, XML) to add to this list at Runtime.

    ' Create a log file for debug logging:
    Private _LogFile As System.IO.StreamWriter = My.Computer.FileSystem.OpenTextFileWriter(My.Computer.FileSystem.SpecialDirectories.Temp.ToString + "\CacheMyWork_Log.txt", True)

    ' This function is used later in UserAppsToSelect_Load to determine if my apps with windows are Visible
    <Runtime.InteropServices.DllImport("user32.dll", SetLastError:=True)> Private Shared Function IsWindowVisible(ByVal hwnd As IntPtr) As Boolean
    End Function

    ''' Upon initialization, this Tool enumerates all processes, then removes those that don't have a visible Window and those not started by the current User.
    ''' The Tool will then enumerate all files held open by the User's visible applications.
    ''' The Tool presents a Dialog showing the User's applications as well as the documents held open by those applications.
    ''' The Tool will note any applications (and documents) selected by the User, and write entries to the User's RunOnce registry key for each application (and/or document).

    Private Sub UserAppsToSelect_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        ' Performance tracing - Log the current date & time at the start of the Tool:
        _LogFile.WriteLine("========= Start time = " + Date.Now)

        BuildListOfProcessesToShowUser()

        ' === UI CODE ===
        ' This block of code is here to populate the strings that are displayed in the CheckListBox.
        Dim _Application As New System.Diagnostics.Process

        UIApplicationsList.CheckOnClick = True
        UIApplicationsList.Sorted = True

        ' TODO: Append the unique "Key" string (from each entry in the UserAppsToDisplay collection) in addition to the non-unique "process.ProcessName" string, to provide unique listings (and indexes).
        For Each _Application In ProcessesToShowUser

            ' First add the resulting list of processes to the CheckedListBox in the Form:
            ' RESEARCH: is there a way to extract the Key value from the Collection, given the position in the Collection or the indexed item's name?
            ' RESEARCH: determine if there's a way to get a more Descriptive name than the cryptic ProcessName (already checked WMI Win32_Process)
            UIApplicationsList.Items.Add(_Application.ProcessName)

        Next _Application

        ' Performance tracing - Log the current date & time:
        _LogFile.WriteLine("========= Finished populating the Form @ " + Date.Now)

    End Sub

    Private Sub UserAppsToSelect_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
        _LogFile.Close()
    End Sub

    Private Sub btnCancel_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCancel.Click
        Me.Close()
    End Sub

    Private Sub btnOK_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnOK.Click
        ' Once the user clicks OK, write the selected apps (& documents) into the user's RunOnce Registry key to launch the apps at next Logon.

        ' Performance tracing - Log the current date & time:
        _LogFile.WriteLine("========= Start populating Registry with chosen Apps @ " + Date.Now)

        ' First convert each checked item from the CheckedListBox to a Process object:
        Dim _CheckedItem As String

        For Each _CheckedItem In UIApplicationsList.CheckedItems

            ' Retrieve process in UserAppsToDisplay collection whose index Key = CheckedItem:
            Dim _RegistryValue As String

            ' TODO: cleanup this duplication of UserAppsToDisplay() calls, and see if CheckedItem and UserAppsToDisplay are appropriate names
            _RegistryValue = GetProcessPropertyFromWMI(ProcessesToShowUser.Item(_CheckedItem.ToString), "CommandLine")

            ' Then write each app to the HKCU RunOnce registry key as a separate entry, using the app's current command line
            My.Computer.Registry.SetValue("HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce", _CheckedItem, _RegistryValue)

            ' TODO: should I just collapse the RegistryValue creation into the following WriteLine() method, rather than do the two steps?
            _LogFile.WriteLine("The process " + ProcessesToShowUser.Item(_CheckedItem.ToString).ProcessName + " has a command line = " + _RegistryValue.ToString)
        Next _CheckedItem

        ' Performance tracing - Log the current date & time:
        _LogFile.WriteLine("========= Finished populating Registry @ " + Date.Now)

        Me.Close()

    End Sub

    Private Sub UIApplicationsList_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles UIApplicationsList.MouseMove
        ' When mouse moves over the CheckedListBox, we'll display the descriptive name for the App over which the mouse pointer moves
        ' TODO: see if there's a way to capture the MouseHover Event and use the IndexFromPoint to find out which App is the hover "target".
        ' TODO: IMPORTANT - find source of CPU consumption when mouse is hovering or moving over the CheckedListBox
        Dim _Point As New Point(e.X, e.Y)
        Dim _Index As Integer
        Dim _ProcessDescriptiveName As String = ""
        Dim _ProcessName As String = ""
        Dim _Process As New Process

        Try
            _Index = Me.UIApplicationsList.IndexFromPoint(_Point.X, _Point.Y)

            ' First, retrieve the process name for the item currently under the mouse pointer:
            ' TODO: BUG - fix the following expression, as it's currently just capturing the ProcessName
            _ProcessName = Me.UIApplicationsList.GetItemText(Me.UIApplicationsList.Items(_Index))

            ' Next, lookup the Descriptive Name for that process and set the Tooltip value to that String:
            ' Note: I'm nesting the lookup of the Process object by the "name" index inside the GetProcessDescriptiveName method.  This indirect method may need to be shortened.
            UIAppsToolTip.SetToolTip(Me.UIApplicationsList, GetProcessDescriptiveName((ProcessesToShowUser(_ProcessName))))

        Catch ex As ArgumentOutOfRangeException
            UIAppsToolTip.SetToolTip(Me.UIApplicationsList, String.Empty)

        End Try

    End Sub

    Private Sub BuildListOfProcessesToShowUser()

        ' Enumerate all current running processes and stuff them in ProcessesToShowUser collection:
        For Each Process In System.Diagnostics.Process.GetProcesses
            ' Note: the Collection's "Key" is created from process' name, so that I can index back into the Collection with just the processname String.
            Try
                ProcessesToShowUser.Add(Process, Process.ProcessName)

            Catch ex As System.ArgumentException
                ' HACK: just catch this exception and move on - the assumption being, the important apps to cache for next time only run one at a time.  The only exception I can think of is a browser, but it's most important to cache the open web pages, and that's already mostly covered by functionality in IE7 & Firefox today.
                ' NOTE: I realize this is a really crappy workaround, but I'm hoping that by the time it's important I'll have figured out a solution to this problem.

                ' In cases where there are two or more processes with the same name, we need to create a unique value
                ' First, count the number of processes with that name:

                'Dim DuplicateProcesses As Array
                'Dim Count As Integer = 0
                'DuplicateProcesses = System.Diagnostics.Process.GetProcessesByName(Process.ProcessName)
                'Count = DuplicateProcesses.Length

                ' Then count the number of such processes already added to the ProcessesToShowUser:
                ' Ideally, we could just ask the Collection how many items are named the same as Process.ProcessName - but I can't figure out how
                ' Should I be using a different Collection type or some other Object class?

            End Try

        Next Process

        ' Remove all processes that do not create a visible Window:
        RemoveProcessesWithoutWindows(ProcessesToShowUser)
        'MessageBox.Show("Processes with Windows = " + ProcessesToShowUser.Count.ToString)

        ' Performance tracing - Log the current date & time:
        _LogFile.WriteLine("========= Finished removing processes that do not have a Window @ " + Date.Now)

        ' Remove the well-known processes that should never be loaded by RunOnce:
        RemoveWellKnownProcesses(ProcessesToShowUser)
        'MessageBox.Show("Processes without well-knowns = " + ProcessesToShowUser.Count.ToString)

        ' TODO: Remove other apps that are already listed in the HKLM Run or HKCU Run keys
        'RemoveProcessesAlreadyAutoStarted(ProcessesToShowUser)

        ' Remove those processes that were not started by me, since I won't be able to (or won't want to) restart such processes next time anyway:
        ' TODO: Determine if there's any difference between the results with and without this "not started by Me" function.
        RemoveProcessesNotStartedByMe(ProcessesToShowUser)
        'MessageBox.Show("Processes started by me = " + ProcessesToShowUser.Count.ToString)

        ' Performance tracing - Log the current date & time:
        _LogFile.WriteLine("========= Finished removing processes *not* started by Me @ " + Date.Now)

        ' Performance tracing - Log the current date & time:
        _LogFile.WriteLine("========= Finished creating final collection of processes for display @ " + Date.Now)

        ' TODO: v2 - check whether any of the ProcessesToShowUser are child processes of any other processes in the collection.  If so, Remove the child process.
        ' (e.g. dexplore.exe is a child of devenv.exe, and can't typically be started on its own.)

        ' TODO: v2 - Enumerate all documents held open by the resulting list of Apps, and cache them ahead of time so that the Tool can display them anytime the user selects the App name in the CheckedListBox.
        ' e.g. use process.startinfo.arguments for some apps

        ' TODO: v3 - Look for ways to return to my "place" in cached documents (e.g. use bookmarks in Word)

    End Sub

    Private Sub RemoveProcessesNotStartedByMe(ByRef Processes As Collection)
        ' Setup username comparison variables:
        Dim _MyUsername As String = My.User.Name

        ' Enumerate all processes, then check whether each process was started by Me:
        Dim _ProcessUserName As String = ""

        For Each Process In Processes
            _ProcessUserName = GetProcessOwnerFromWMI(Process)

            _LogFile.WriteLine("The process " + Process.ProcessName + " runs as " + _ProcessUserName + ".")

            ' Compare the Process' username to the current logged-in user - if it's the same, then we assume the process was
            '   started by the logged-in user, and should be considered a candidate for auto-restart i.e. left in the array:
            If _ProcessUserName <> _MyUsername Then
                Processes.Remove(Process.ProcessName)
            Else
                _LogFile.WriteLine("This process " + Process.ProcessName + " (aka " + Process.MainWindowTitle + ") is mine.")
            End If
        Next Process

    End Sub

    Private Sub RemoveProcessesWithoutWindows(ByRef Processes As Collection)

        For Each Process In Processes

            Try

                If Process.MainWindowHandle = 0 Then
                    Processes.Remove(Process.ProcessName)
                Else
                    _LogFile.WriteLine("My process " + Process.ProcessName + " (aka " + GetProcessDescriptiveName(Process) + ") has a Window.")
                End If

            Catch ex As System.ComponentModel.Win32Exception
                ' e.g. "Access Denied" error when Process.MainWindowHandle fails because the user doesn't have sufficient privilege
                ' to the processes they don't own.  This even fails on Vista when user is Protected Admin - only elevated Admin can get past this.
                ' TODO: figure out if I could re-order the functions so that this occurs *after* the RemoveProcessesNotStartedByMe() call, so that this function never tries to open a handle to a "foreign" process.

            End Try
        Next Process

        ' TODO: add capability for user to configure Registry setting that toggles the inclusion of Invisible Windows (default = not included).
        ' TODO: !! Fix the IsWindowVisible call so I can implement it (and avoid listing the DWM process)
        'RemoveProcessesWithoutVisibleWindows(Processes)

        '' Performance tracing - Log the current date & time:
        '_LogFile.WriteLine("========= Finished removing processes that do not have a visible Window @ " + Date.Now)

    End Sub

    Private Sub RemoveProcessesWithoutVisibleWindows(ByRef Processes As Collection)
        ' Remove those processes whose Window is not visible:
        For Each Process In Processes
            If IsWindowVisible(Process.Handle) = False Then
                Processes.Remove(Process.ProcessName)
            Else
                _LogFile.WriteLine("The process " + Process.ProcessName + " has a visible Window.")
            End If
        Next Process

    End Sub

    Private Sub RemoveWellKnownProcesses(ByRef Processes As Collection)
        ' HACK: this original approach may not be needed (esp. Try...Catch...) since removal works one at a time
        ' (Original approach was based on collecting the apps during each cycle, and using each cycle to filter the list from the previous cycle.)
        'For Each Process In MyAppsWithWindows
        '    If Not Array.BinarySearch(AppsNeverDisplayed, Process.ProcessName.ToString) Then
        '        '' *** This Try...Catch is here because I'm adding the processes for the CheckListBox to the public Collection, 
        '        ''       and I've encountered an exception when there are two same-named processes.
        '        Try
        '            ' Note: the Collection's Key is created from process' name, so that I can index back into the Collection with just the processname String.
        '            UserAppsToDisplay.Add(Process, Process.ProcessName)
        '            LogFile.WriteLine("The app " + Process.ProcessName + " is in the filtered list.")
        '        Catch ex As System.ArgumentException
        '            ' Handle the error when > 1 process with the same ProcessName are found()
        '            Dim i As Integer
        '            ' TODO: complete the multiple-process exception handling
        '            ' 1. Count the number of processes in the Collection with the same ProcessName

        '            ' 2. Add the process to the Collection with a numbered Index
        '            UserAppsToDisplay.Add(Process, Process.ProcessName + "(" + i.ToString + ")")
        '        End Try
        '    End If
        'Next

        ' Search the array of apps that should never be displayed and eliminate any matches:
        For Each Process In Processes
            For Each App As String In AppsNeverDisplayed
                If App.Contains(Process.ProcessName) Then
                    Processes.Remove(Process.ProcessName)
                Else
                    _LogFile.WriteLine("The app " + Process.ProcessName + " is not on the 'forbidden' list :).")
                End If
            Next App
        Next Process

    End Sub

    Private Sub RemoveProcessesAlreadyAutoStarted(ByRef Processes As Collection)
        ' e.g. Pull in all values from HKLM Run & HKCU Run keys:
        Dim _HKLMRunValues, _HKCURunValues As New ArrayList
        Const _HKLMRunKey As String = "HKey_Local_Machine\Software\Microsoft\Windows\CurrentVersion\Run"
        Const _HKCURunKey As String = "HKey_Current_User\Software\Microsoft\Windows\CurrentVersion\Run"
        '' RESEARCH: would it be better to pull in the existing Registry values and make the comparison in memory [= memory bloat], or compare each process to the Registry values directly [= multiple Registry reads]?




    End Sub

    Function GetProcessPropertyFromWMI(ByRef Process As Process, ByVal ProcessAttribute As String) As String
        Dim _ReturnValue As String = ""
        Dim ProcessID As Integer = Process.Id
        ' Setup queryable WMI object for that process:
        Dim _searcher As New Management.ManagementObjectSearcher("SELECT * FROM Win32_Process WHERE ProcessId = " & ProcessID)
        For Each queryObj As Management.ManagementObject In _searcher.Get
            _ReturnValue = queryObj(ProcessAttribute)
        Next queryObj

        Return _ReturnValue

    End Function

    Function GetProcessOwnerFromWMI(ByRef Process As Process) As String

        ' This function returns the DOMAIN\username that owns the passed-in Process.
        Dim _ProcessID As Integer = Process.Id
        Dim _ProcessUserName As String = ""
        Dim _ProcessUserDomain As String = ""

        ' Select queryable WMI object for that Process:
        Dim searcher As New Management.ManagementObjectSearcher("SELECT * FROM Win32_Process WHERE ProcessId = " & _ProcessID)

        For Each queryObj As Management.ManagementObject In searcher.Get
            Dim _ReturnedElements As Management.ManagementBaseObject = queryObj.InvokeMethod("GetOwner", Nothing, Nothing)
            _ProcessUserName = _ReturnedElements("User").ToString
            _ProcessUserDomain = _ReturnedElements("Domain").ToString
        Next

        Return _ProcessUserDomain + "\" + _ProcessUserName

    End Function


    Function GetProcessDescriptiveName(ByVal Process As Process) As String
        Dim _ProcessDescriptiveName As String

        _ProcessDescriptiveName = Process.MainModule.FileVersionInfo.FileDescription

        Return _ProcessDescriptiveName

    End Function



    ' TODO: v1 - Display the Descriptive Name for each process in the UI, not its short Process Name.
    ' TODO: v1 - Separate the non-UI code from the UI-oriented UserAppsToSelect Class.
    ' TODO: v2 - Eventually I'd like to add the current open Documents as a one-to-many relation with the process
    ' TODO: v2 - When app is selected and its open docs are displayed, enable all document checkboxes by default (i.e. re-open all currently-open docs for "cached apps" by default).
    ' RESEARCH: why do I get "Object reference not set to an instance of an object." if I try to cache "devenv"?

End Class
